use std::collections::HashMap;
use std::env;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};

fn main() {
    let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing manifest dir"));
    let tests_dir = crate_dir.join("tests");

    if !tests_dir.is_dir() {
        println!(
            "cargo:rerun-if-changed={}",
            crate_dir.join("tests").display()
        );
        return;
    }

    let mut discovered = Vec::new();
    collect_test_files(&tests_dir, &tests_dir, &mut discovered)
        .expect("failed to scan tests directory");
    discovered.sort();

    let out_path = tests_dir.join("auto_tests.rs");
    let mut file = File::create(&out_path).expect("failed to create auto_tests.rs");

    writeln!(file, "// @generated by build.rs. Do not edit.\n")
        .expect("failed to write auto_tests header");

    let mut suffix_counts: HashMap<String, usize> = HashMap::new();
    for rel in &discovered {
        if rel.file_name().and_then(|s| s.to_str()) == Some("auto_tests.rs") {
            continue;
        }
        if rel.extension().and_then(|s| s.to_str()) != Some("rs") {
            continue;
        }

        let rel_str = rel.to_string_lossy().replace('\\', "/");
        let module_name = unique_ident(rel, &mut suffix_counts);

        writeln!(file, "#[allow(non_snake_case)]").expect("failed to write attribute");
        writeln!(file, "mod {} {{", module_name).expect("failed to start module");
        writeln!(
            file,
            "    include!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/tests/{}\"));",
            rel_str
        )
        .expect("failed to write include");
        writeln!(file, "}}\n").expect("failed to end module");
    }

    println!("cargo:rerun-if-changed={}", tests_dir.display());
    for rel in &discovered {
        println!("cargo:rerun-if-changed={}", tests_dir.join(rel).display());
    }
}

fn collect_test_files(base: &Path, dir: &Path, acc: &mut Vec<PathBuf>) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        if path
            .file_name()
            .and_then(|s| s.to_str())
            .map(|s| s.starts_with('.'))
            .unwrap_or(false)
        {
            continue;
        }
        if path.is_dir() {
            collect_test_files(base, &path, acc)?;
        } else if let Ok(rel) = path.strip_prefix(base) {
            acc.push(rel.to_path_buf());
        }
    }
    Ok(())
}

fn unique_ident(rel: &Path, suffix_counts: &mut HashMap<String, usize>) -> String {
    let raw = rel.to_string_lossy();
    let trimmed = raw.strip_suffix(".rs").unwrap_or(&raw);
    let mut ident: String = trimmed
        .chars()
        .map(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' => c,
            _ => '_',
        })
        .collect();

    while ident.contains("__") {
        ident = ident.replace("__", "_");
    }
    if ident.ends_with('_') {
        ident.pop();
    }
    if ident.is_empty() {
        ident.push_str("test");
    }
    if ident
        .chars()
        .next()
        .map(|c| c.is_ascii_digit())
        .unwrap_or(true)
    {
        ident.insert(0, '_');
    }

    let counter = suffix_counts.entry(ident.clone()).or_insert(0);
    if *counter == 0 {
        *counter = 1;
        ident
    } else {
        let new_ident = format!("{}_{counter}", ident);
        *counter += 1;
        new_ident
    }
}
